/*
 *      This program is free software; you can redistribute it and/or modify
 *      it under the terms of the GNU General Public License as published by
 *      the Free Software Foundation; either version 2 of the License, or
 *      (at your option) any later version.
 *      
 *      This program is distributed in the hope that it will be useful,
 *      but WITHOUT ANY WARRANTY; without even the implied warranty of
 *      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *      GNU General Public License for more details.
 *      
 *      You should have received a copy of the GNU General Public License
 *      along with this program; if not, write to the Free Software
 *      Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *      MA 02110-1301, USA.
 */

package bgtp;

import java.util.*;
import java.nio.*;
import java.net.*;
import java.nio.channels.*;
import java.io.IOException;


/**
 *читаем парсим и пишем HTTP поверх произвольных каалов передачи
 */
public class OverHTTP
{
    private String		rowDelim	 = "\r\n";
    private String		headerFieldDelim = ": ";
    private Transport		channel		 = null;
    private BGTPLog		logger		 = null;
    private String		globalBuffer	 = "";
    private Vector<String>	sendOrder;

    private final int	ST_RHEAD = 0;
    private final int	ST_RBODY = 1;
    private final int	ST_ERROR = 2;
    private final int	ST_FIN	 = 3;
    private final int	ST_HANDL = 4;
    private int		state	 = 0;

    /**
     *@param connection экземпляр типа Transport реализующий низкоуровневую передачу
     */
    public OverHTTP(Transport connection)
    {
	channel	  = connection;
	logger	  = new BGTPLog("OverHTTP");
	sendOrder = new Vector<String>();
    }

    /**
     *закрыть текущее соединение
     */
    public void close()
    {
	channel.close();
	channel = null;
    }
    
    /**
     *настроить канал на блокирующий/неблокирущий режим работы
     *@param newVal новое состояние
     *@return признак успешности операции
     */
    public boolean setBlocking(boolean newVal)
    {
	return channel.setBlocking(newVal);
    }

    /**
     *принять HTTP пакет
     *@return словарь содержащий все заголовки хттп + 2а специальный ключа для получения 
     *типа запроса/ответа и тела ReqType и Body соответственно
     */
    public TreeMap<String, String> recv() throws IOException
    {
	TreeMap<String, String> tmp	   = null;
	String			buffer	   = globalBuffer;
	int			breaded	   = 0;
	String			errmsg	   = "";
	int			shouldread = 0;
	boolean			readend	   = false;
	String			tailBuffer = "";
	String			item;
	String[] arr;

	state = ST_RHEAD;

	while(readend == false)
	    {
		switch(state)
		    {
		    case ST_RHEAD:
			//logger.debug("recv{ST_RHEAD}", "----------");
			// if(buffer.indexOf(rowDelim+rowDelim) > 0)
			//     {
			// 	channel.setBlocking(false);
			//     }
			// else
			//     {
			// 	channel.setBlocking(true);
			//     }

			buffer += channel.readBlock(1024);
			if(channel.count() < 0)
			    {
				//надо выбрасывать эксепшн
				throw(new IOException("Connection break"));
			    }

			breaded += channel.count();

			//ограничим длину заголовков 4я килобайтами
			if(breaded > 4096)
			    {
				state  = ST_ERROR;
				errmsg = "HTTP header is over big "+breaded+":"+buffer.length();
				break;
			    }

			if(breaded == 0 & buffer.length() == 0)
			    {
				//а ничо не пришло
				state = ST_FIN;
				break;
			    }

			//если заголовок прочитан
			//System.out.println("header.buffer='"+buffer+"'");
			if(buffer.indexOf(rowDelim+rowDelim) > 0)
			    {
				state = ST_HANDL;
			    }
			break;

		    case ST_HANDL:
			//logger.debug("recv{ST_HANDL}", "----------");
			tmp	   = new TreeMap<String, String>();
 			arr	   = buffer.split(rowDelim+rowDelim);

			if(arr.length >= 2) // есть тушка либо еще чего
			    {
				tailBuffer = buffer.substring(arr[0].length()+rowDelim.length()*2);
			    }
			//System.out.println("buffer='"+buffer+"' len="+buffer.length());
			//System.out.println("tailBuffer='"+tailBuffer+"' len="+tailBuffer.length());
			buffer = arr[0];
			arr    = buffer.split(rowDelim);

			String[] keyValue;

			tmp.put("ReqType", arr[0]);
			for(int i = 1; i < arr.length; i++)
			    {
				item	 = arr[i];
				keyValue = item.split(headerFieldDelim);
				tmp.put(keyValue[0], keyValue[1]);
			    }
			
			state = ST_FIN;
			//если есть тело то буим его читать
			if(tmp.containsKey("Content-Length") == true)
			    {
				state	   = ST_RBODY;
				shouldread = Integer.parseInt(tmp.get("Content-Length"));
				breaded	   = 0;
 			    }
			buffer = tailBuffer;
			break;

		    case ST_RBODY:
			//logger.debug("recv{ST_RBODY}", "----------");
			if(buffer.length() < shouldread)
			    {
				buffer	+= channel.readBlock(shouldread - buffer.length());
				breaded += channel.count();
				
				if(channel.count() < 0)
				    {
					//надо выбрасывать эксепшн
					throw(new IOException("Connection break at body read"));
				    }
			    }
			else
			    {
				breaded = shouldread;
			    }

			//System.out.println("breaded="+breaded+" shouldread="+shouldread+" buffer.len="+buffer.length());
			if(breaded >= shouldread)
			    {
				state  = ST_FIN;
				//System.out.println("substring='"+buffer.substring(0, shouldread)+"'");
				//System.out.println("buffer='"+buffer+"'");
				tmp.put("Body", buffer.substring(0, shouldread));
				buffer = buffer.substring(shouldread);
				
				shouldread = 0;
			    }
			break;

		    case ST_ERROR:
			logger.error("recv", errmsg);
			logger.info("recv", buffer);
			state = ST_FIN;
			tmp	= null;
			break;

		    case ST_FIN:
			//logger.debug("recv{ST_FIN}", "----------");
			if(buffer.length() > 0)
			    {
				globalBuffer = buffer;
			    }
			readend = true;
			break;

		    default:
			break;
		    }
		//logger.debug("recv", "State "+state);
	    }

	return tmp;
    }

    /**
     *отослать данные
     *@param data экземпляр TreeMap содержащий данные для отсылки
     *должны быть обязательные для протокола поля(см. спецификацию)
     */
    public void send(TreeMap<String, String> data) throws IOException
    {
	sendOrder.clear();
	String	headString = "";
	boolean hasBody	   = false;

	for(String key: data.keySet())
	    {
		if(key.equals("Body") == false && key.equals("ReqType") == false)
		    {
			sendOrder.add(key);
		    }
	    }

	//выыглядит пиздецово но я чет запутался с логическими выражениями
	//ночные тупки
	if(data.containsKey("Body") == true)
	    {
		if(data.get("Body") != null)
		    {
			if(data.get("Body").length() > 0)
			    {
				hasBody = true;
			    }
		    }
	    }

	if(hasBody == true)
	    {
		data.put("Content-Length", ""+data.get("Body").length());
	    }

	headString += data.get("ReqType")+rowDelim;

	for(String key: sendOrder)
	    {
		headString += key+headerFieldDelim+data.get(key)+rowDelim;
		if(headString.length() > 1024)
		    {
			if(channel.writeBlock(headString) < 0)
			    {
				throw new IOException("Connecrion break: at header partial write");
			    }
			headString = "";
		    }
	    }

	headString += rowDelim;
	if(channel.writeBlock(headString) < 0)
	    {
		throw new IOException("Connecrion break: at header write");
	    }

	if(hasBody == true)
	    {
		if(channel.writeBlock(data.get("Body")) < 0)
		    {
			throw new IOException("Connection break: at write body");
		    }
	    }
    }
}